1. Introduction
From the Book :
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
This pattern provides a solution when client code needs to be able to restore the state of an object at a certain point in time. It doesn't need to know about the internal implementation of that object, it just keeps a black box with all the
The Memento pattern is sometimes used in combination with commands (Command pattern) to implement an Undo and Redo mechanism.
2. Example - Task with Authority Matrix
Let's consider an Authority Matrix where people are assigned roles and must take a decision whether the task can go ahead.
Role | Description |
---|---|
Accountable | The single person with this role owns the process and bears the final responsibility. |
Responsible | Persons with this role bear a partial responsibility. They are responsible for particular aspects or steps in a process. |
Consulted | This role belongs to persons that are consulted for there knowledge, but bear no active responsibility in the execution of the process. |
Informed | These role belong to people that are kept informed of the process |
We will implement a class Task that represents a process execution - e.g. approving a new IT project. Several persons have to take a decision in order for the project to be approved.
We could refine Task by automatically setting the decision for CONSULTED and INFORMED to INDIFFERENT. For illustrating the Memento pattern, this is not important.
A scenario that fits in with our example and where we might be interested in using mementos is when we want to print an overview of the past decisions (look for the whyNot method). It is a little contrived, but it is a simple scenario to show how things work.
3. Code
3.1. Classes
The first class is a simple representation of a person with a name. We also provide a copy method that returns a deep copy of our person. We don't want to keep a Memento where other code can keep a reference to the person instance and change the name. Later we will show a unit test that illustrates the point.
package be.ooxs.examples.designpatterns.memento; public class Person { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public Person copy() { Person copy = new Person(); copy.setName(name); return copy; } }
Here is the basic implementation of Memento as seen by client code. That's right, we just intend to keep the reference around, there isn't much more to it.
package be.ooxs.examples.designpatterns.memento; public class Memento { }
Here are some enums used to distinguish roles and decision types:
package be.ooxs.examples.designpatterns.memento; public enum AuthorityMatrixType { RESPONSIBLE, ACCOUNTABLE, CONSULTED, INFORMED, }
package be.ooxs.examples.designpatterns.memento; public enum DecisionType { UNDEFINED, AGREE, DISAGREE, INDIFFERENT, POSTPONE, }
Next take a look at the Decision nothing very peculiar. It also has a copy method for producing deep copies. And the same unit test illustrates the point.
package be.ooxs.examples.designpatterns.memento; class Decision { private AuthorityMatrixType role; private Person person; private DecisionType decisionType = DecisionType.UNDEFINED; private String motivation; private Decision() { } public Decision(Person person, AuthorityMatrixType role) { super(); this.person = person; this.role = role; } public void set(DecisionType decisionType, String motivation) { this.decisionType = decisionType; this.motivation = motivation; } public Person getPerson() { return person; } public DecisionType getDecisionType() { return decisionType; } public String getMotivation() { return motivation; } public AuthorityMatrixType getRole() { return role; } public boolean complete() { return !DecisionType.UNDEFINED.equals(decisionType); } protected Decision copy() { Decision copy = new Decision(); copy.person = this.person.copy(); copy.role = this.role; copy.decisionType = this.decisionType; copy.motivation = this.motivation; return copy; } }
And here is the most interesting class, Task . The memento part consists of a static inner class MementoImpl that extends Memento . MementoImpl and its instance variables are completely hidden from client code. Memento is a black box.
Client code just invokes createMemento end setMemento . That's it. Task and MementoImpl together are responsible for keeping enough data in the MementoImpl to restore everything to the state of a particular point in time.
package be.ooxs.examples.designpatterns.memento; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.Map.Entry; public class Task { private static Set<DecisionType> GO_AHEAD_TYPES = new TreeSet<DecisionType>(); static { GO_AHEAD_TYPES.add(DecisionType.AGREE); GO_AHEAD_TYPES.add(DecisionType.INDIFFERENT); } private Person accountable; private Map<Person, Decision> decisions = new HashMap<Person, Decision>(); public void registerPerson(Person person, AuthorityMatrixType role) { Decision decision = new Decision(person, role); if (AuthorityMatrixType.ACCOUNTABLE.equals(role)) { if (accountable == null) { accountable = person; } else { throw new IllegalArgumentException("Only one person in role 'Accountable' is allowed."); } } decisions.put(person, decision); } public void makeDecision(Person person, DecisionType decisionType, CharSequence motivation) { decisions.get(person).set(decisionType, motivation.toString()); } public boolean complete() { for (Decision decision : decisions.values()) { if (!decision.complete()) { return false;//at least one person didn't decide yet } } return true; } public boolean canGoAhead() { for (Decision decision : decisions.values()) { if (!GO_AHEAD_TYPES.contains(decision.getDecisionType())) { return false; } } return true; } public void whyNot(Appendable out) throws IOException { out.append("Participants decided against this task:\n"); for (Decision decision : decisions.values()) { if (!GO_AHEAD_TYPES.contains(decision.getDecisionType())) { if (DecisionType.UNDEFINED.equals(decision.getDecisionType())) { out.append(decision.getPerson().getName()); out.append(" has not decided yet.\n\n"); } else { out.append(decision.getPerson().getName()); out.append(" decided "); out.append(decision.getDecisionType().toString()); out.append(" and motivated with \n\t'"); out.append(decision.getMotivation()); out.append("'\n\n"); } } } } public Memento createMemento() { MementoImpl result = new MementoImpl(); result.copyStateFrom(this); return result; } public void setMemento(Memento memento) { ((MementoImpl) memento).copyStateTo(this); } static class MementoImpl extends Memento { private Person accountable; private Map<Person, Decision> decisions = new HashMap<Person, Decision>(); private void copyStateFrom(Task task) { for (Entry<Person, Decision> entry : task.decisions.entrySet()) { Decision decisionCopy = entry.getValue().copy(); Person personCopy = decisionCopy.getPerson(); if (AuthorityMatrixType.ACCOUNTABLE.equals(decisionCopy.getRole())) { this.accountable = personCopy; } this.decisions.put(personCopy, decisionCopy); } } private void copyStateTo(Task task) { task.accountable = accountable; task.decisions = decisions; } } }
3.2. Unit Test
Very long test methods are not a best practice. This method ( testMemento ) is intended to illustrate rather than test.
The different task.whyNot(System.out) statements are there for illustrating what is happening to the task . It is generally a bad idea to keep those kind of statements around in unit tests - they generate too much 'noise' in server logs etc...
package be.ooxs.examples.designpatterns.memento; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; import org.junit.Before; import org.junit.Test; public class UTestTask { Person p1, p2, p3, p4; Task task; @Before public void setUp() { p1 = createPerson("Mr. A"); p2 = createPerson("Ms. B"); p3 = createPerson("Mrs. C"); p4 = createPerson("Mr. D"); task = new Task(); } private Person createPerson(String string) { Person person = new Person(); person.setName(string); return person; } @Test public void testMemento() throws IOException { task.registerPerson(p1, AuthorityMatrixType.ACCOUNTABLE); task.registerPerson(p2, AuthorityMatrixType.CONSULTED); task.registerPerson(p3, AuthorityMatrixType.RESPONSIBLE); task.registerPerson(p4, AuthorityMatrixType.RESPONSIBLE); //step 1 task.makeDecision(p1, DecisionType.AGREE, "OK"); task.makeDecision(p2, DecisionType.POSTPONE, "Budget not yet approved"); task.makeDecision(p3, DecisionType.DISAGREE, "Too expensive."); assertFalse(task.complete()); assertFalse(task.canGoAhead()); Memento step1 = task.createMemento(); task.whyNot(System.out); //step 2 task.makeDecision(p4, DecisionType.AGREE, "OK"); assertTrue(task.complete()); assertFalse(task.canGoAhead()); Memento step2 = task.createMemento(); task.whyNot(System.out); //step 3 task.makeDecision(p3, DecisionType.AGREE, "New price acceptable."); assertTrue(task.complete()); assertFalse(task.canGoAhead()); Memento step3 = task.createMemento(); task.whyNot(System.out); //step 4 task.makeDecision(p2, DecisionType.AGREE, "Budget approved."); assertTrue(task.complete()); assertTrue(task.canGoAhead()); Memento step4 = task.createMemento(); //Use memento to go back to previous states: //return to step 1 task.setMemento(step1); assertFalse(task.complete()); assertFalse(task.canGoAhead()); task.whyNot(System.out); //return to step 2 task.setMemento(step2); assertTrue(task.complete()); assertFalse(task.canGoAhead()); task.whyNot(System.out); //return to step 3 task.setMemento(step3); assertTrue(task.complete()); assertFalse(task.canGoAhead()); //return to step 4 task.setMemento(step4); assertTrue(task.complete()); assertTrue(task.canGoAhead()); task.whyNot(System.out); } }
Here is the output from that test run:
Participants decided against this task: Mrs. C decided DISAGREE and motivated with 'Too expensive.' Mr. D has not decided yet. Ms. B decided POSTPONE and motivated with 'Budget not yet approved' Participants decided against this task: Mrs. C decided DISAGREE and motivated with 'Too expensive.' Ms. B decided POSTPONE and motivated with 'Budget not yet approved' Participants decided against this task: Ms. B decided POSTPONE and motivated with 'Budget not yet approved' Participants decided against this task: Mrs. C decided DISAGREE and motivated with 'Too expensive.' Ms. B decided POSTPONE and motivated with 'Budget not yet approved' Mr. D has not decided yet. Participants decided against this task: Mrs. C decided DISAGREE and motivated with 'Too expensive.' Ms. B decided POSTPONE and motivated with 'Budget not yet approved' Participants decided against this task:
To show that our whyNot method can be used for assertions, look at the last three statements. Here we use java.util.regex.Pattern to check for an expected phrase in the result. The (?s) ('DOTALL') part in the regular expression causes the matcher to match 'newlines' for '.' too.
In the context of unit tests, whenever you try to assert large chunks of text contain some fragment, try to check for the smallest possible part. It makes your test more robust. Extra whitespace, changing or correcting a few words is less likely to break the test.
In this case we check for the presence of the fragment 'Budget not yet approved'. Which is largely sufficient.
@Test public void testDisagree() throws IOException { task.registerPerson(p1, AuthorityMatrixType.ACCOUNTABLE); task.registerPerson(p2, AuthorityMatrixType.RESPONSIBLE); task.makeDecision(p1, DecisionType.AGREE, "OK"); task.makeDecision(p2, DecisionType.DISAGREE, "Budget not yet approved"); assertFalse(task.canGoAhead()); StringBuilder buffer = new StringBuilder(); task.whyNot(buffer); assertTrue("Expected person 2 to disagree '" + buffer + "'", Pattern.matches("(?s).*Budget not yet approved.*", buffer)); }
This test method tests whether a name change of the person is also restored when using a memento.
@Test public void testMementoKeepsOriginalName() throws IOException { task.registerPerson(p1, AuthorityMatrixType.ACCOUNTABLE); task.registerPerson(p2, AuthorityMatrixType.RESPONSIBLE); task.makeDecision(p1, DecisionType.AGREE, "OK"); task.makeDecision(p2, DecisionType.DISAGREE, "Budget not yet approved"); Memento memento = task.createMemento(); p2.setName("Mrs. Z"); StringBuilder buffer = new StringBuilder(); task.whyNot(buffer); assertTrue("Expected 'Mrs. Z' in '" + buffer + "'", Pattern.matches("(?s).*Mrs\\. Z.*", buffer)); task.setMemento(memento); buffer = new StringBuilder(); task.whyNot(buffer); assertTrue("Expected 'Ms. B again' in '" + buffer + "'", Pattern.matches("(?s).*Ms\\. B.*", buffer)); }